package airacrobats.game.object;

import airacrobats.io.data.AirplaneData;
import airacrobats.io.data.FlapsData;
import airacrobats.io.data.GhostData;
import airacrobats.io.data.JWavefrontModel;
import airacrobats.sound.Sound;
import airacrobats.sound.Sound3D;
import airacrobats.util.Util;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.util.ArrayList;
import java.util.List;
import javax.media.opengl.GL2;
import javax.media.opengl.GLAutoDrawable;
import javax.media.opengl.glu.GLU;
import javax.media.opengl.glu.GLUquadric;
import javax.sound.sampled.LineUnavailableException;
import javax.swing.JPanel;
import javax.vecmath.Point2f;
import javax.vecmath.Point3f;
import javax.vecmath.Quat4f;
import javax.vecmath.Vector3f;
import javax.vecmath.Vector4f;
import jogamp.opengl.glu.GLUquadricImpl;

/**
 * This class describes an airplane.
 */
public class Airplane extends GameObject
{
    /**
     * The maximum angle of the flaps.
     */
    public final static float MAX_FLAPS_ANGLE = 35.0f;
    /**
     * The maximum pitch.
     */
    public final static float MAX_PITCH = 0.134f;
    /**
     * The maximum roll.
     */
    public final static float MAX_ROLL = 0.3f;
    /**
     * This is the distance (meters) where the airplane real engine sound can't be heard anymore.
     */
    private static final float SOUND_MAX_DISTANCE = 1200.0f;
    /**
     * Size of each display panel.
     */
    private static final Dimension DISPLAY_PANEL_SIZE = new Dimension(62, 70);
    /**
     * Distance warning from the border.
     */
    private static final float BORDER_LENGHT_WARNING = 220.0f;
    /**
     * If the airplane x-z reaches this value, the stage fails.
     */
    private static final float BORDER_LENGHT = 250.0f;

    /**
     * The current engine sound.
     */
    private Sound3D engineSound;
    /**
     * Speed vector.
     */
    private Vector3f speedVector;
    /**
     * Last orientation of the airplane.
     */
    private Vector4f lastOrientation;
    /**
     * Scale factor.
     */
    private float scale;
    /**
     * The airplane data.
     */
    private AirplaneData airplaneData;
    /**
     * The roll value.
     */
    private float roll;
    /**
     * The pitch value.
     */
    private float pitch;
    /**
     * The engine power gain (1-100).
     */
    private int enginePowerGain;
    /**
     * The speed module.
     */
    private float speed;
    /**
     * A list with all the airplane locations, that will be recorded.
     */
    private List<GhostData> ghosts;
    /**
     * Flag indicating if the smoke is turned on/off.
     */
    protected boolean smokeOn;
    /**
     * The collision sphere radius.
     */
    private float collisionRadius;
    /**
     * The collision spheres.
     */
    private Point3f[] collisionSpheres;
    /**
     * Random factor used on the calculus of the cruise movement.
     */
    private int cruiseFactor;
    /**
     * Flaps models.
     */
    private JWavefrontModel[] flapsModels;
    /**
     * The propeller model.
     */
    private JWavefrontModel propellerModel;
    /**
     * Flaps data list.
     */
    private FlapsData[] flapsDataList;

    /**
     * @param model The model.
     * @param shadowModel The shadow model of the airplane.
     * @param flapModels The airplane flaps.
     * @param propellerModel The propeller model, may be null.
     * @param flapsDataList The flaps data.
     * @param position The initial position.
     * @param airplaneData The airplane data.
     * @param speedVector The initial speed vector.
     * @param inverted Inverted flag.
     * @param engineSounds The engine sounds.
     */
    public Airplane(JWavefrontModel model, JWavefrontModel shadowModel, JWavefrontModel[] flapModels,
            JWavefrontModel propellerModel, FlapsData[] flapsDataList, Point3f position, AirplaneData airplaneData,
            Vector3f speedVector, boolean inverted, Sound3D[] engineSounds)
    {
        super(model, shadowModel, position, engineSounds);
        this.flapsModels = flapModels;
        this.propellerModel = propellerModel;
        this.flapsDataList = flapsDataList;
        this.airplaneData = airplaneData;
        this.scale = airplaneData.getScaleFactor() / UNITARY_SIZE;
        this.speedVector = speedVector;
        this.lastOrientation = new Vector4f(speedVector);
        this.lastOrientation.normalize();
        this.enginePowerGain = 100;

        if (inverted)
        {
            // The airplane will start looking at the oposite side.
            Quat4f q = new Quat4f(0.0f, 0.0f, 1.0f, 0.0f);
            rQuaternion.mul(q);
            q = new Quat4f(1.0f, 0.0f, 0.0f, 0.0f);
            rQuaternion.mul(q);
        }

        ghosts = new ArrayList<GhostData>();

        // Five: center, north, south, east, west.
        collisionSpheres = new Point3f[5];
        createCollisionSpheres();

        cruiseFactor = (int) (Math.random() * 100);
    }

    /**
     */
    public void setupInfoPanel(JPanel owner)
    {
        JPanel panel = null;

        panel = new JPanel()
        {
            @Override
            protected void paintComponent(Graphics g)
            {
                paintAltitudeDisplay(g);
            }
        };
        panel.setPreferredSize(DISPLAY_PANEL_SIZE);
        panel.setMinimumSize(DISPLAY_PANEL_SIZE);
        owner.add(panel);

        panel = new JPanel()
        {
            @Override
            protected void paintComponent(Graphics g)
            {
                paintAttitudeDisplay(g);
            }
        };
        panel.setPreferredSize(DISPLAY_PANEL_SIZE);
        panel.setMinimumSize(DISPLAY_PANEL_SIZE);
        owner.add(panel);

        panel = new JPanel()
        {
            @Override
            protected void paintComponent(Graphics g)
            {
                paintSpeedDisplay(g);
            }
        };
        panel.setPreferredSize(DISPLAY_PANEL_SIZE);
        panel.setMinimumSize(DISPLAY_PANEL_SIZE);
        owner.add(panel);

        panel = new JPanel()
        {
            @Override
            protected void paintComponent(Graphics g)
            {
                paintEngine(g);
            }
        };
        panel.setPreferredSize(DISPLAY_PANEL_SIZE);
        panel.setMinimumSize(DISPLAY_PANEL_SIZE);
        owner.add(panel);

        panel = new JPanel()
        {
            @Override
            protected void paintComponent(Graphics g)
            {
                paintCoords(g);
            }
        };
        panel.setPreferredSize(DISPLAY_PANEL_SIZE);
        panel.setMinimumSize(DISPLAY_PANEL_SIZE);
        owner.add(panel);
    }

    /**
     * Sets the pitch and roll values, so the airplane will keep rotating.
     * @param roll The deltaX of the movement.
     * @param pitch The deltaY of the movement.
     */
    public void setRotation(float roll, float pitch)
    {
        this.pitch = pitch;
        this.roll = roll;
    }

    /**
     * @param smokeOn The smoke on flag.
     */
    public void setSmokeOn(boolean smokeOn)
    {
        this.smokeOn = smokeOn;
    }

    /**
     * @return The smoke on flag.
     */
    public boolean isSmokeOn()
    {
        return smokeOn;
    }

    /**
     * @param enginePower The engine power.
     */
    public void setEnginePower(int enginePower)
    {
        this.enginePowerGain = enginePower;
    }

    /**
     * @return The engine power.
     */
    public int getEnginePower()
    {
        return enginePowerGain;
    }

    /**
     */
    @Override
    public void update(long deltaTime, long time, Point3f cameraPosition) throws Exception
    {
        speed = speedVector.length();
        super.update(deltaTime, time, cameraPosition);
    }

    /**
     */
    @Override
    public float getSpeed()
    {
        return speed;
    }

    /**
     * @return The ghosts.
     */
    public List<GhostData> getGhosts()
    {
        return ghosts;
    }

    /**
     * @return True if the airplane is out of the flight area.
     */
    public boolean isOutOfArea()
    {
        return (Math.abs(position.x) > BORDER_LENGHT || Math.abs(position.z) > BORDER_LENGHT);
    }

    /**
     * Checks the collision between this airplane and the given terrain.
     * @param terrain The terrain.
     * @return The collision result.
     */
    public boolean isCollidingWith(Terrain terrain)
    {
        Vector3f normal = new Vector3f();
        Point3f normalPosition = new Point3f(position);
        normalPosition.scale(1 / Terrain.LENGTH);
        
        float height = 1.0f + Terrain.SHADOW_MIN_HEIGHT +
                Terrain.LENGTH * terrain.calculatePointFace(normalPosition, normal);
        
        return position.y < height;
    }

    /**
     * Checks if this airplane collides with the given airplane.
     * @param airplane The airplane.
     * @return The collision result.
     */
    public boolean isCollidingWith(Airplane airplane)
    {
        for (Point3f sphere : collisionSpheres)
        {
            Point3f firstPoint = new Point3f(sphere.x, sphere.y, sphere.z);
            rMatrixInverse.transform(firstPoint);
            firstPoint.add(position);

            for (Point3f secondSphere : airplane.collisionSpheres)
            {
                Point3f secondPoint = new Point3f(secondSphere.x, secondSphere.y, secondSphere.z);
                airplane.rMatrixInverse.transform(secondPoint);
                secondPoint.add(airplane.position);
                
                if (secondPoint.distance(firstPoint) < collisionRadius * 2)
                {
                    return true;
                }
            }
        }
        return false;
    }

    /**
     */
    @Override
    public void transformModelview(GL2 gl)
    {
        gl.glMultMatrixf(new float[]
        {
            rMatrixInverse.m00, rMatrixInverse.m01, rMatrixInverse.m02, rMatrixInverse.m03,
            rMatrixInverse.m10, rMatrixInverse.m11, rMatrixInverse.m12, rMatrixInverse.m13,
            rMatrixInverse.m20, rMatrixInverse.m21, rMatrixInverse.m22, rMatrixInverse.m23,
            rMatrixInverse.m30, rMatrixInverse.m31, rMatrixInverse.m32, rMatrixInverse.m33
        }, 0);
    }

    /**
     */
    @Override
    public void compileModels(GLAutoDrawable gLAutoDrawable, int mode)
    {
        super.compileModels(gLAutoDrawable, mode);
        for (JWavefrontModel flapsModel : flapsModels)
        {
            flapsModel.compile(gLAutoDrawable, mode);
        }
        if (propellerModel != null)
            propellerModel.compile(gLAutoDrawable, mode);
    }

    /**
     */
    protected void doDraw(GLAutoDrawable drawable, JWavefrontModel model)
    {
        GL2 gl = drawable.getGL().getGL2();

        gl.glTranslatef(position.x, position.y, position.z);
        gl.glScalef(scale, scale, scale);

        gl.glMultMatrixf(new float[]
        {
            rMatrix.m00, rMatrix.m01, rMatrix.m02, rMatrix.m03,
            rMatrix.m10, rMatrix.m11, rMatrix.m12, rMatrix.m13,
            rMatrix.m20, rMatrix.m21, rMatrix.m22, rMatrix.m23,
            rMatrix.m30, rMatrix.m31, rMatrix.m32, rMatrix.m33
        }, 0);

        model.draw(drawable);

        // Checks if the model to draw is the airplane itself (not the shadow).
        if (model == this.model)
        {
            // OBS: The shadow does not have flaps or propeller.

            float rollAngle = roll * MAX_FLAPS_ANGLE / MAX_ROLL;
            float pitchAngle = pitch * MAX_FLAPS_ANGLE / MAX_PITCH;
            
            int i = 0;
            for (JWavefrontModel flapsModel : flapsModels)
            {
                gl.glPushMatrix();
                gl.glTranslatef(flapsDataList[i].getTranslation().x,
                        flapsDataList[i].getTranslation().y, flapsDataList[i].getTranslation().z);

                if (flapsDataList[i].getPitchFactor() != 0)
                    gl.glRotatef(flapsDataList[i].getPitchFactor() * pitchAngle, 0.0f, 0.0f, 1.0f);
                if (flapsDataList[i].getRollFactor() != 0)
                    gl.glRotatef(flapsDataList[i].getRollFactor() * rollAngle, 0.0f, 0.0f, 1.0f);

                i++;
                flapsModel.draw(drawable);
                gl.glPopMatrix();
            }

            if (propellerModel != null)
            {
                gl.glPushMatrix();
                gl.glTranslatef(airplaneData.getPropellerPosition().x,
                        airplaneData.getPropellerPosition().y, airplaneData.getPropellerPosition().z);
                propellerModel.draw(drawable);
                gl.glPopMatrix();
            }

            /*drawCollisionSphere(gl, collisionSpheres[0]);
            drawCollisionSphere(gl, collisionSpheres[1]);
            drawCollisionSphere(gl, collisionSpheres[2]);
            drawCollisionSphere(gl, collisionSpheres[3]);
            drawCollisionSphere(gl, collisionSpheres[4]);*/
        }
    }

    /**
     * Draws the collision sphere. For test purposes only.
     * @param gl The gl object.
     * @param point The point of the collision sphere.
     */
    private void drawCollisionSphere(GL2 gl, Point3f point)
    {
        gl.glTranslatef(point.x, point.y, point.z);
        GLU glu = new GLU();
        GLUquadric q = new GLUquadricImpl(gl, false);
        glu.gluSphere(q, collisionRadius, 32, 32);
        gl.glTranslatef(-point.x, -point.y, -point.z);
    }

    /**
     * Creates the airplane collision spheres.
     */
    private void createCollisionSpheres()
    {
        Point3f highest = null;
        Point3f lowerest = null;
        Point3f northest = null;
        Point3f southest = null;
        Point3f westest = null;
        Point3f eastest = null;

        float[] vertices = model.getVertices();

        for (int i = 0; i < model.getVertices().length - 3; i += 3)
        {
            if (highest == null || vertices[i + 1] > highest.y)
            {
                highest = new Point3f(vertices[i], vertices[i + 1], vertices[i + 2]);
            }
            if (lowerest == null || vertices[i + 1] < lowerest.y)
            {
                lowerest = new Point3f(vertices[i], vertices[i + 1], vertices[i + 2]);
            }
            if (northest == null || vertices[i] > northest.x)
            {
                northest = new Point3f(vertices[i], vertices[i + 1], vertices[i + 2]);
            }
            if (southest == null || vertices[i] < southest.x)
            {
                southest = new Point3f(vertices[i], vertices[i + 1], vertices[i + 2]);
            }
            if (eastest == null || vertices[i + 2] > eastest.z)
            {
                eastest = new Point3f(vertices[i], vertices[i + 1], vertices[i + 2]);
            }
            if (westest == null || vertices[i + 2] < westest.z)
            {
                westest = new Point3f(vertices[i], vertices[i + 1], vertices[i + 2]);
            }
        }

        collisionRadius = (highest.y - lowerest.y) / 2 * scale;
        collisionSpheres[0] = new Point3f(northest.x - collisionRadius, airplaneData.getXAxis().y, airplaneData.getXAxis().z);
        collisionSpheres[1] = new Point3f(southest.x + collisionRadius, airplaneData.getXAxis().y, airplaneData.getXAxis().z);
        collisionSpheres[2] = new Point3f(airplaneData.getZAxis().x, airplaneData.getXAxis().y, westest.z + collisionRadius);
        collisionSpheres[3] = new Point3f(airplaneData.getZAxis().x, airplaneData.getXAxis().y, eastest.z - collisionRadius);
        collisionSpheres[4] = new Point3f(airplaneData.getZAxis().x, airplaneData.getXAxis().y, airplaneData.getXAxis().z);

        for (Point3f sphere : collisionSpheres)
        {
            sphere.scale(scale);
        }
    }

    /**
     * @throws LineUnavailableException Thrown if an error occurs whiles playing the sound.
     */
    protected void doUpdate(long deltaTime, long time, Point3f cameraPosition) throws LineUnavailableException
    {
        updateSound(cameraPosition);
        rotate(deltaTime);
        move(time, deltaTime);
        ghosts.add(new GhostData(new Quat4f(rQuaternion), new Point3f(position), time, smokeOn));
    }

    /**
     * Updates the airplane sounds.
     * @param cameraPosition The camera position, to set the gain/pan of the sound.
     * @throws LineUnavailableException
     */
    private void updateSound(Point3f cameraPosition) throws LineUnavailableException
    {
        // Just an extra protection.
        if (sounds.length == 0 || cameraPosition == null)
            return;
        
        int step = 100 / (sounds.length);
        int currentStep = (enginePowerGain - 1) / step;

        if (enginePowerGain == 0)
        {
            if (engineSound != null)
                engineSound.stop();
            engineSound = null;
        }
        else if (engineSound != sounds[currentStep])
        {
            if (engineSound != null)
                engineSound.stop();
            engineSound = sounds[currentStep];
            engineSound.play();
        }

        if (engineSound != null)
        {
            float distance = position.distance(cameraPosition) * UNITARY_SIZE;

            engineSound.setGain(
                    (Sound.MIN_GAIN - Sound.MAX_GAIN) / SOUND_MAX_DISTANCE * distance + Sound.MAX_GAIN);
            engineSound.setPan(new Point2f(position.x, position.z),
                    rMatrixInverse, new Point2f(cameraPosition.x, cameraPosition.z));
        }
    }

    /**
     * Applies the stick rotations.
     * @param deltaTime The delta time.
     */
    private void rotate(long deltaTime)
    {
        // Calculates maximum speed.
        float maximumSpeed = airplaneData.getMaximumSpeedFactor() * airplaneData.getCruiseSpeed();

        // Factor to reduce the roll (Using maximum speed as 1.2f * maximum airplane speed).
        float factor = speed > 0.6f * maximumSpeed ? 1.0f : 1.67f / maximumSpeed * speed;

        float w = roll * factor * deltaTime / 1000000000.0f;
        Quat4f q = new Quat4f(2 * w, 0.0f, 0.0f, w * w - 1);
        rQuaternion.mul(q);

        // Factor to reduce the pitch (Using maximum speed as 1.2f * maximum airplane speed).
        factor = - 4.0f / (float) Math.pow(1.2f * maximumSpeed, 2) *
                (float) Math.pow(speed - maximumSpeed / 2.0f, 2) + 1;
        
        // Question: The 'cos' is also applied on the quaternion z, this avoids control lose after
        // a 180 degrees rolling. WHY?!?
        w = pitch * factor * deltaTime / 1000000000.0f;
        float cc = (float) Math.cos(roll);
        cc *= cc;
        q = new Quat4f(0.0f, 0.0f, 2 * cc * w, cc * (w * w - 1));
        rQuaternion.mul(q);
    }

    /**
     * Applies the movements. Updates the velocities and orientations.
     * @param time The time.
     * @param deltaTime The delta time.
     */
    private void move(long time, long deltaTime)
    {
        // Calculates the engine power vector.
        Vector4f engineAcceleration = new Vector4f(airplaneData.getEnginePower() * enginePowerGain / 100.0f, 0.0f, 0.0f, 0.0f);
        rMatrixInverse.transform(engineAcceleration);

        // Calculates the orientation vector.
        Vector4f orientationTransform = new Vector4f(1.0f, 0.0f, 0.0f, 0.0f);
        rMatrixInverse.transform(orientationTransform);

        // Calculates the delta orientation from the last loop call.
        Vector4f differenceVector = new Vector4f(orientationTransform);
        differenceVector.sub(lastOrientation);

        // Saves the current orientation.
        lastOrientation = orientationTransform;

        // The speed factor is how much speed the airplane will get from the delta angles.
        // 1.74 is empirical.
        float speedFactorY = (float) Math.pow(speed, 1.7);
        float speedFactor = (float) Math.pow(speed, 1.7);
        // Adds 'energy' transfer. The 0.025 value is empirical.
        speedVector.x += differenceVector.x * speedFactor * 0.025f;
        speedVector.y += differenceVector.y * speedFactorY * 0.025f;
        speedVector.z += differenceVector.z * speedFactor * 0.025f;

        // Calculates the angle between the airplane and the horizontal plane.
        Vector4f angleVector = new Vector4f(0.0f, -1.0f, 0.0f, 0.0f);
        float angle = orientationTransform.angle(angleVector);
        float sustentationAngle = Math.max(0.05f, (float) Math.abs(Math.PI / 2 - angle));

        // If the airplane is looking up, reduces it power so it can fall.
        if (sustentationAngle > Math.PI / 3.0f && orientationTransform.y > 0)
        {
            engineAcceleration.y -= (float) Math.sin(sustentationAngle) * airplaneData.getEnginePower() / 1.5f;
        }

        // Horizontal plane speed vector.
        Vector3f speedVectorEx = new Vector3f(speedVector.x, 0.0f, speedVector.z);

        // Calculates the acceleration gain of the sustentation.
        // Note that the sustentation also depends on the horizontal speed.
        float sustentationAcceleration = (float) Math.cos(sustentationAngle) * 9.8f *
                (speedVectorEx.lengthSquared() / airplaneData.getCruiseSpeed() / airplaneData.getCruiseSpeed());

        // Calculates the air break acceleration.
        float airBreakAcceleration = 0;
        if (speed < airplaneData.getCruiseSpeed())
        {
            airBreakAcceleration = 20.0f / airplaneData.getCruiseSpeed() * speed;
        }
        else
        {
            airBreakAcceleration = 9.8f / (airplaneData.getMaximumSpeedFactor() - 1.0f) /
                    airplaneData.getCruiseSpeed() * speed + 20.0f - 9.8f /
                    (airplaneData.getMaximumSpeedFactor() - 1.0f);
        }

        // Creates the normalized speed vector, to apply the air break.
        Vector3f normalSpeedVector = new Vector3f(speedVector);
        normalSpeedVector.normalize();

        float yAcceleration = engineAcceleration.y - 9.8f + sustentationAcceleration -
                airBreakAcceleration * normalSpeedVector.y;

        // Loop acceleration attenuation.
        if (yAcceleration < 0 && engineAcceleration.y > 0)
            yAcceleration /= 2.0f;
        
        // Applies the accelerations over the speed vectors.
        speedVector.x += (engineAcceleration.x - airBreakAcceleration * normalSpeedVector.x)
                * deltaTime / 1000000000.0f;
        speedVector.y += (yAcceleration) * deltaTime / 1000000000.0f;
        speedVector.z += (engineAcceleration.z - airBreakAcceleration * normalSpeedVector.z)
                * deltaTime / 1000000000.0f;
        
        // Applies the speed over the position vector.
        position.x += speedVector.x * deltaTime / 1000000000.0f / UNITARY_SIZE;
        position.y += speedVector.y * deltaTime / 1000000000.0f / UNITARY_SIZE;
        position.z += speedVector.z * deltaTime / 1000000000.0f / UNITARY_SIZE;

        // Applying the cruise turbulence. The cruise turbulence is only applied above 10 units of altitude.
        // It is constant, no matter the angle of your airplane. TODO: change this.
        if (time > 0 && position.y > 1.0f)
            position.y += Math.sin(time / 1000.0f + cruiseFactor) / (500.0f);
    }

    /**
     * Paints the engine power.
     * @param g The graphics.
     */
    private void paintEngine(Graphics g)
    {
        int size = DISPLAY_PANEL_SIZE.width / 2;
        int startX = (DISPLAY_PANEL_SIZE.width - size) / 2;
        int startY = 0;

        Util.drawVerticalBar(Color.GREEN.darker(), Color.BLACK, g, startX,
                startY, size, size * 2, enginePowerGain, 2);
    }

    /**
     * Paints the speed display.
     * @param g The graphics.
     */
    private void paintSpeedDisplay(Graphics g)
    {
        int startX = 0;
        int startY = 0;
        int radius = DISPLAY_PANEL_SIZE.width / 2 - 1;

        Util.drawDisplay(Color.BLACK, Color.WHITE, g, startX, startY, radius, 90);

        String speedS = Integer.toString((int) speed) + "m/s";
        Util.drawOutlinedString(speedS, Color.WHITE, Color.BLACK, g,
                startX + radius - g.getFontMetrics().stringWidth(speedS) / 2,
                startY + radius + (int) (g.getFontMetrics().getHeight() * 0.3f));
    }

    /**
     * Paints the altitude display.
     * @param g The graphics.
     */
    private void paintAltitudeDisplay(Graphics g)
    {
        int startX = 0;
        int startY = 0;
        int radius = DISPLAY_PANEL_SIZE.width / 2 - 1;

        Util.drawDisplay(Color.BLACK, Color.WHITE, g, startX, startY, radius, 30);

        String feet = Integer.toString((int) (position.y * UNITARY_SIZE / 0.3048f)) + "ft";
        Util.drawOutlinedString(feet, Color.WHITE, Color.BLACK, g,
                startX + radius - g.getFontMetrics().stringWidth(feet) / 2,
                startY + radius + (int) (g.getFontMetrics().getHeight() * 0.3f));
    }

    /**
     * Paints the attitude display.
     * @param g The graphics.
     */
    private void paintAttitudeDisplay(Graphics g)
    {
        int startX = 0;
        int startY = 4;
        int radius = DISPLAY_PANEL_SIZE.width / 2 - 1;

        Util.drawSquareDisplay(Color.BLACK, Color.WHITE, g, startX, startY - 4, radius * 2 + 8, radius * 2, 10);
        Util.drawDisplay(Color.BLACK, Color.WHITE, g, startX, startY, radius, 90);

        // Here, it applies some math to show the attitude display.
        // The details will not be explained here.
        Vector4f v = new Vector4f(0.0f, 0.0f, (radius - 5), 1.0f);
        rMatrixInverse.transform(v);

        Vector3f vEx = new Vector3f(v.x, v.y, v.z);
        float angle = vEx.angle(new Vector3f(v.x, 0.0f, v.z));

        double tan = Math.tan(angle);

        double x = (radius - 5) * (radius - 5) / (1 + tan * tan);
        x = Math.sqrt(x);
        double y = x * tan;
        y = v.y > 0 ? -y : y;

        v = new Vector4f((radius - 5), 0.0f, 0.0f, 1.0f);
        rMatrixInverse.transform(v);

        // Draws the calculated lines with the math applied above.
        g.setColor(Color.WHITE);
        g.drawLine(startX + radius, startY + radius + (int) v.y,
                startX + radius + (int) x, startY + radius + (int) y + (int) v.y);
        g.drawLine(startX + radius, startY + radius + (int) v.y,
                startX  + radius - (int) x, startY + radius - (int) y + (int) v.y);
        g.drawLine((int) (startX + radius * 0.4), startY + radius,
                (int) (startX + radius * 1.6), startY + radius);
    }

    /**
     * Paints the coordinates of the airplane.
     * @param g The graphics.
     */
    private void paintCoords(Graphics g)
    {
        int startX = 0;
        int startY = 0;
        int size = DISPLAY_PANEL_SIZE.width / 2 - 2;

        int height = g.getFontMetrics().getHeight();

        Util.drawSquareDisplay(Color.BLACK, Color.WHITE, g, startX, startY, height * 2 + 4, size * 2, 0);

        Color color = Color.WHITE;

        if (Math.abs(position.x) > BORDER_LENGHT_WARNING)
            color = Color.RED;

        Util.drawOutlinedString("X: " + (int) (position.x * UNITARY_SIZE) + "m",
                color, Color.BLACK, g, startX + 3, startY + height);

        color = Color.WHITE;

        if (Math.abs(position.z) > BORDER_LENGHT_WARNING)
            color = Color.RED;
        
        Util.drawOutlinedString("Z: " + (int) (position.z * UNITARY_SIZE) + "m",
                color, Color.BLACK, g, startX + 3, startY + height * 2 + 1);
    }
}
